<?php
/* 文字图像
*
*
*/

namespace hpnWse {

require_once(\hpnWse\fGetWseDiry() . 'hpnWse/SqlUtil.php');
require_once(\hpnWse\fGetWseDiry() . 'hpnWse/HttpSvc.php');

use \hpnWse\stNumUtil;
use \hpnWse\stStrUtil;
use \hpnWse\stAryUtil;
use \hpnWse\stObjUtil;
use \hpnWse\stSqlUtil;
use \hpnWse\stHttpSvc;


/// 分页缓存
class tPgntCch
{
	public static $sc_HitCnt = 0; // 命中数

	public $c_SqlForRcds; // 用于读取记录的SQL
	public $c_SqlForTot; // 用于读取总数的SQL
	public $c_TrkDiry; // 轨道目录，末尾不带“/”
	public $c_PageCpct; // 分页容量
	public $c_MaxTot; // 最大总数
	public $c_RfshItvl; // 索引刷新间隔
	public $c_RcdTot; // 记录总数，获取时若为null则重新计算，否则复用

	public $c_IdxRcd; // 索引记录

	/// 初始化
	/// a_Cfg: Object
	/// {
	/// c_Sql：String，SQL，当缓存未命中或过期，则用PDO执行查询，
	/// 	【警告：格式必须为“SELECT ... FROM ...”，大写FROM，不能含有LIMIT子句】
	/// c_StoDiry: String，储存目录，必须有效
	/// c_Trk：String，轨道，对应a_Cfg[c_StoDiry]里的目录，缓存分页文件储存在此，默认null表示md5(c_Sql)
	/// c_PageCpct：Number，分页容量（数据库记录数），默认100，
	///		【警告：请事先考虑好合适的值并坚持使用，一旦改变了这个值，必须清空轨道目录，否则会产生混乱！】
	/// c_MaxTot：Number，最大总数，默认0表示不做限制
	/// c_RfshItvl: Number，刷新间隔（秒），默认3
	/// }
	public function __construct($a_Cfg)
	{
		$this->c_MaxTot = stObjUtil::cFchPpty($a_Cfg, 'c_MaxTot', 0);

		$this->c_SqlForRcds = $a_Cfg['c_Sql'];
	//	$l_Sql = mb_strtoupper($this->c_SqlForRcds);
		$l_FromIdx = stStrUtil::cFind($this->c_SqlForRcds, 'FROM');
		$this->c_SqlForTot = 'SELECT COUNT(*) ';
		$this->c_SqlForTot .= stStrUtil::cSub($this->c_SqlForRcds, $l_FromIdx);
		if ($this->c_MaxTot > 0)
		{
			$this->c_SqlForTot .= ' LIMIT ';
			$this->c_SqlForTot .= $this->c_MaxTot;
		}

		$l_StoDiry = stStrUtil::cEnsrDiry($a_Cfg['c_StoDiry']);	
		$l_Trk = stObjUtil::cFchPpty($a_Cfg, 'c_Trk');
		if (!$l_Trk) { $l_Trk = md5($this->c_SqlForRcds); }
		$this->c_TrkDiry = $l_StoDiry . $l_Trk;
		$this->c_PageCpct = stObjUtil::cFchPpty($a_Cfg, 'c_PageCpct', 100);
		
		$this->c_RfshItvl = stObjUtil::cFchPpty($a_Cfg, 'c_RfshItvl', 3);

		$this->c_IdxRcd = null;
	}

	/// 缓存作废，索引文件变成“{"c_Tot":-1,"c_Mts":[]}”
	public static function scCclTrk($a_StoDiry, $a_Trk)
	{
		$l_Temp = new tPgntCch(array(
			'c_Sql' => 'SELECT * FROM',
			'c_StoDiry' => $a_StoDiry,
			'c_Trk' => $a_Trk
		));
		$l_Temp->cCcl();
	}

	/// 获取记录
	/// a_Bgn: Number, 起始索引≥0
	/// a_Amt: Number，数量＞0
	/// 返回：Object[]，即stSqlUtil::cPrprExecFchAll
	public function cFchRcds($a_Bgn, $a_Amt)
	{
		// 无效范围？
		if (($a_Bgn < 0) || ($a_Amt <= 0))
		{ return array(); }

		// 判断轨道目录是否存在，如果不存在，创建之
		//【注意：mkdir()本身能够保证线程安全（由操作系统负责）】
		$l_TrkDiry = $this->c_TrkDiry;
		$l_TrkExi = file_exists($l_TrkDiry);
		if (!$l_TrkExi)
		{
			// 这是阻塞调用，即使返回false也不要紧（暗示目录可能刚刚被另一线程创建）
			// 但若还不存在，说明发生比较严重的错误，直接抛出异常
			mkdir($l_TrkDiry);
			if (!file_exists($l_TrkDiry))
			{
				throw new \Exception('无法创建分页缓存轨道目录“' . $l_TrkDiry . '/”！', -1);
			}
		}

		// 按需刷新索引
		if (null === $this->c_IdxRcd)
		{
			$this->cRfshIdx();
		}

		// 现在索引已就位，转交内部
		return $this->eFchRcds($a_Bgn, $a_Amt);
	}

	/// 刷新索引
	/// a_Mode: Number，模式，0=读内存、读文件、重建；1=读文件、重建，2=重建
	public function cRfshIdx($a_Mode = 0)
	{
		if ((null !== $this->c_IdxRcd) && (0 == $a_Mode))
		{ return $this; }

		$l_ReadRst = (2 !== $a_Mode) ? $this->eReadIdxRcd() : false;
		$l_FceRcrt = false;
		if (false !== $l_ReadRst)
		{
			$this->c_IdxRcd = stObjUtil::cDcdJson($l_ReadRst);
			if ($this->c_IdxRcd['c_Tot'] >= 0) // 若作废则为-1
			{ return $this; }

			$l_FceRcrt = true; // 强制重建
		}

		$this->eRcrtIdx(1, $l_FceRcrt);
		return $this;
	}

	/// 获取记录总数
	public function cGetRcdTot()
	{
		// 按需刷新索引
		if (null === $this->c_IdxRcd)
		{
			$this->cRfshIdx();
		}
		return $this->c_IdxRcd['c_Tot'];
	}

	/// 缓存作废（总会强制重建索引）
	public function cCcl()
	{
		$this->eRcrtIdx(-1, true);
		$this->c_IdxRcd = null;
		return $this;
	}

	// 读取索引记录，返回false或String
	private function eReadIdxRcd()
	{
		// fstat
		// Array
		// (
		//     [dev] => 771
		//     [ino] => 488704
		//     [mode] => 33188
		//     [nlink] => 1
		//     [uid] => 0
		//     [gid] => 0
		//     [rdev] => 0
		//     [size] => 1114
		//     [atime] => 1061067181(s)
		//     [mtime] => 1056136526(s)
		//     [ctime] => 1056136526(s)
		//     [blksize] => 4096
		//     [blocks] => 8
		// )

		$l_Rst = false; // 默认失败
		$l_Path = $this->c_TrkDiry . '/_.json';
		
		try
		{ $l_FH = file_exists($l_Path) ? fopen($l_Path, 'r') : null; }
		catch (\Exception $a_Exc)
		{ $l_FH = null; }

		if (!$l_FH || !flock($l_FH, LOCK_SH)) // 共享锁
		{ return false; }

		//【警告：确保缓存的数据还在保质期】
		$l_Stas = fstat($l_FH);
		$l_Now = time();
		if ($l_Now - $l_Stas['mtime'] < $this->c_RfshItvl)
		{
			$l_Rst = fread($l_FH, intval($l_Stas['size']));
		}

		flock($l_FH, LOCK_UN);
		fclose($l_FH);
		return $l_Rst;
	}

	// 重建索引文件，
	// a_For: -1=作废, 1=总数，2=修改时间
	private function eRcrtIdx($a_For, $a_Fce = false)
	{
		$l_Rst = true; // 默认成功
		$l_Path = $this->c_TrkDiry . '/_.json';
		
		try //【注意这里应该用“c”，参阅文档】
		{ $l_FH = @fopen($l_Path, 'c'); }
		catch (\Exception $a_Exc)
		{ $l_FH = null; }

		if (!$l_FH || !flock($l_FH, LOCK_EX)) // 独占锁
		{ return false; }

		//【警告：这里必须再次检查！避免重复写入（可能另一个线程刚刚写入，若是自己创建的则大小为0）】
		$l_Stas = fstat($l_FH);
		$l_Now = time();
		if ($a_Fce ||
			(0 == $l_Stas['size']) || 
			($l_Now - $l_Stas['mtime'] >= $this->c_RfshItvl))
		{
			if (-1 == $a_For)
			{
				$this->c_IdxRcd = array(
					'c_Tot' => -1,
					'c_Mts' => array()
				);
			}
			else
			if (1 == $a_For)
			{
				$l_Pdo = stHttpSvc::cCnctToDb();
				$l_Tot = stSqlUtil::cPrprExecFchNum($l_Pdo, $this->c_SqlForTot, null);
				$this->c_IdxRcd = array(
					'c_Tot' => $l_Tot,
					'c_Mts' => stAryUtil::cNew(ceil($l_Tot / $this->c_PageCpct), 0)
				);
			}

			ftruncate($l_FH, 0);
			$l_Rst = @fwrite($l_FH, stObjUtil::cEcdJson($this->c_IdxRcd));
			fflush($l_FH);
		}

		flock($l_FH, LOCK_UN);
		fclose($l_FH);
		return (false === $l_Rst) ? false : true;
	}

	// 获取
	private function eFchRcds($a_Bgn, $a_Amt)
	{
		// 利用记录总数，确保请求范围有效
		$l_Tot = $this->cGetRcdTot();
		if ($a_Bgn >= $l_Tot)
		{ return array(); }

		if ($a_Bgn + $a_Amt > $l_Tot)
		{ $a_Amt = $l_Tot - $a_Bgn; }

		// 计算请求的分页索引范围，获取并按需重建
		$l_PageCpct = $this->c_PageCpct;
	//	$l_PageTot = ceil($l_Tot / $l_PageCpct);
		$l_PageBgn = floor($a_Bgn / $l_PageCpct);
		$l_PageEnd = floor(($a_Bgn + $a_Amt - 1) / $l_PageCpct);
		$l_PageAmt = $l_PageEnd - $l_PageBgn + 1;
		return $this->eFcnAndRcrtIfNeed($a_Bgn, $a_Amt, $l_PageBgn, $l_PageAmt);
	}

	// 重建分页
	private function eRcrtPages($a_PageBgn, $a_PageAmt)
	{
		// 从数据库读取对应整数个页面的纪录
		$l_PageCpct = $this->c_PageCpct;
		$l_PageRcdBgn = $a_PageBgn * $l_PageCpct;
		$l_PageRcdAmt =  $a_PageAmt * $l_PageCpct;
		$l_Rcds = $this->eReadDb($this->c_SqlForRcds, $l_PageRcdBgn, $l_PageRcdAmt);
		if (!$l_Rcds) // 可能为空
		{ return array(); }

		// 根据实际记录数，修正范围
		$l_RcdLen = count($l_Rcds);
		$a_PageAmt = ceil($l_RcdLen / $l_PageCpct);

		// 写入分页文件，即使失败了也没有关系（意味着无法缓存，每次都要读数据库）
		for ($p=$a_PageBgn; $p<$a_PageBgn + $a_PageAmt; ++$p)
		{
			$this->eTsWrtFile($p, $l_Rcds);
		}
		
		return $l_Rcds;
	}

	// 获取并按需重建
	private function eFcnAndRcrtIfNeed($a_Bgn, $a_Amt, $a_PageBgn, $a_PageAmt)
	{
		// 先确定需要重建的范围，同时收集保质期内的片段
		$l_PageCpct = $this->c_PageCpct;
		$l_Fgmts = stAryUtil::cNew($a_PageAmt, '[]');
		$l_PageEnd = $a_PageBgn + $a_PageAmt - 1;
		$l_Min = $l_PageEnd + 1;
		$l_Max = $a_PageBgn - 1;
		for ($p=$a_PageBgn; $p<=$l_PageEnd; ++$p)
		{
			// 如果索引记录为0，表示必须更新这一分页
			$l_ReadRst = (0 == $this->c_IdxRcd['c_Mts'][$p]) 
				? false : $this->eTsReadFile($p);

			// 不存在或已过期
			if (false === $l_ReadRst)
			{
				if ($l_Min > $p) { $l_Min = $p; }
				if ($l_Max < $p) { $l_Max = $p; }
			}
			else
			{
				$l_Fgmts[$p - $a_PageBgn] = $l_ReadRst;
			}
		}

		// 如果需要，重建之
		//【警告：理论上，重建后可能会发现记录数缩减！比如数据库大量删除记录，但总数文件还没有更新。
		// 解决方案：
		// 如果分页数减少了，重建索引，然后重新获取，这是因为请求范围可能严重偏离有效范围；
		// 如果分页数没变但记录数减少了，只要重建范围在尾部就如常处理（stAryUtil::cSub会搞定）,
		// 但若在中间，就和分页数减少一样了。
		// 另外注意这里收到的a_Amt可能已经比外界最初提供的更小，这没有关系，因为记录数变少了。】
		if ($l_Min <= $l_PageEnd)
		{
			$l_Amt = $l_Max - $l_Min + 1;
			$l_RcrtRst = $this->eRcrtPages($l_Min, $l_Amt);
			$l_RcrtLen = count($l_RcrtRst);

			if ((ceil($l_RcrtLen / $l_PageCpct) < $l_Amt) ||
				(($l_RcrtLen < $l_Amt * $l_PageCpct) && ($l_Max < $l_PageEnd)))
			{
				$this->eRcrtIdx(1, true);
				return $this->eFchRcds($a_Bgn, $a_Amt);
			}
			
			$l_WrtIdx = false; // 是否需要写入索引记录？
			for ($p=$l_Min; $p<=$l_Max; ++$p)
			{
				$l_Fgmts[$p - $a_PageBgn] = array_slice($l_RcrtRst, 
					($p - $l_Min) * $l_PageCpct, $l_PageCpct);

				if (!$l_WrtIdx && (0 == $this->c_IdxRcd['c_Mts'][$p]))
				{ $l_WrtIdx = true; }
			}

			if ($l_WrtIdx)
			{
				$l_Now = time();
				for ($p=$l_Min; $p<=$l_Max; ++$p)
				{
					if (0 == $this->c_IdxRcd['c_Mts'][$p])
					{ $this->c_IdxRcd['c_Mts'][$p] = $l_Now; }
				}
				$this->eRcrtIdx(2, true);
			}
		}
		else // 命中！
		{
			++self::$sc_HitCnt;
		}

		// 汇总结果
		$l_Rst = array();

		// 首页
		if (is_string($l_Fgmts[0]))
		{ $l_Fgmts[0] = stObjUtil::cDcdJson($l_Fgmts[0]); }
		stAryUtil::cCcatTo($l_Rst, 
			stAryUtil::cSub($l_Fgmts[0], $a_Bgn - $a_PageBgn * $l_PageCpct));

		// 中间
		for ($p=$a_PageBgn+1; $p<=$l_PageEnd-1; ++$p)
		{
			if (is_string($l_Fgmts[$p - $a_PageBgn]))
			{ $l_Fgmts[$p - $a_PageBgn] = stObjUtil::cDcdJson($l_Fgmts[$p - $a_PageBgn]); }
			stAryUtil::cCcatTo($l_Rst, 
				$l_Fgmts[$p - $a_PageBgn]);
		}

		// 尾页
		if ($a_PageAmt > 1)
		{
			if (is_string($l_Fgmts[$a_PageAmt - 1]))
			{ $l_Fgmts[$a_PageAmt - 1] = stObjUtil::cDcdJson($l_Fgmts[$a_PageAmt - 1]); }
			stAryUtil::cCcatTo($l_Rst, 
				stAryUtil::cSub($l_Fgmts[$a_PageAmt - 1], 0, $a_Bgn + $a_Amt - $l_PageEnd * $l_PageCpct));
		}
		
		return $l_Rst;
	}

	private function eGetPagePath($a_Idx)
	{
		return $this->c_TrkDiry . '/' . $a_Idx . '.json';
	}

	// 线程安全读取文件，返回false表示文件不存在或已过期
	private function eTsReadFile($a_PageIdx)
	{
		$l_Rst = false; // 默认失败
		$l_Path = $this->eGetPagePath($a_PageIdx);
		
		try
		{ $l_FH = file_exists($l_Path) ? fopen($l_Path, 'r') : null; }
		catch (\Exception $a_Exc)
		{ $l_FH = null; }

		if (!$l_FH || !flock($l_FH, LOCK_SH)) // 共享锁
		{ return false; }

		// 确保缓存的数据还在保质期
		$l_Stas = fstat($l_FH);
		$l_Now = time();
		if ($l_Now - $l_Stas['mtime'] < $this->c_RfshItvl)
		{
			$l_Rst = fread($l_FH, $l_Stas['size']);
		}

		flock($l_FH, LOCK_UN);
		fclose($l_FH);
		return $l_Rst;
	}

	// 线程安全写入文件，返回false表示失败
	private function eTsWrtFile($a_PageIdx, $a_AllRcds)
	{
		$l_Rst = true; // 默认成功
		$l_PageCpct = $this->c_PageCpct;
		$l_Path = $this->eGetPagePath($a_PageIdx);

		try //【注意这里应该用“c”，参阅文档】
		{ $l_FH = @fopen($l_Path, 'c'); }
		catch (\Exception $a_Exc)
		{ $l_FH = null; }

		if (!$l_FH || !flock($l_FH, LOCK_EX)) // 独占锁
		{ return false; }

		//【警告：这里必须再次检查！避免重复写入（可能另一个线程刚刚写入，若是自己创建的则大小为0）】
		$l_Stas = fstat($l_FH);
		$l_Now = time();
		if ((0 == $l_Stas['size']) || 
			($l_Now - $l_Stas['mtime'] >= 1))
		{
			$l_RcdBgn = $a_PageIdx * $l_PageCpct;
			$l_SubRcds = stAryUtil::cSub($a_AllRcds, $l_RcdBgn, $l_RcdBgn + $l_PageCpct);
			$l_JsonStr = stObjUtil::cEcdJson($l_SubRcds);
			
			ftruncate($l_FH, 0);
			$l_Rst = @fwrite($l_FH, $l_JsonStr);
			fflush($l_FH);
		}

		flock($l_FH, LOCK_UN);
		fclose($l_FH);
		return (false === $l_Rst) ? false : true;
	}

	private function eReadDb($a_Sql, $a_Bgn, $a_Amt)
	{
		$a_Sql .= ' LIMIT ';
		$a_Sql .= $a_Bgn;
		$a_Sql .= ',';
		$a_Sql .= $a_Amt;
		$l_Pdo = stHttpSvc::cCnctToDb();
	//	stHttpSvc::$c_Dbg['c_Sqls'][] = $a_Sql;
		return stSqlUtil::cPrprExecFchAll($l_Pdo, $a_Sql, null, true);
	}
}

} // namespace hpnWse

//////////////////////////////////// OVER ////////////////////////////////////